package de.hpi.generator.model;

import java.io.IOException;

import de.hpi.generator.config.ColumnModel;
import de.hpi.generator.generator.distributors.DataDistributor;
import de.hpi.generator.generator.storages.UniqueValuesDataStorage;
import de.hpi.generator.generator.storages.ValueSeriaInfo;
import de.hpi.generator.generator.storages.ValuesDataStorage;
import de.hpi.generator.helpers.FileHelper;
import de.hpi.generator.model.dependencies.InclusionDependency;
import de.hpi.generator.model.dependencies.InclusionDependencyCollection;
import java.io.EOFException;
import java.io.File;

public class Column {

    /**
     * a name of the column
     */
    private String aName;

    public String getName() {
        return aName;
    }
    /**
     * a size of a column datatype
     */
    protected int aCellSize;

    public int getCellSize() {
        return aCellSize;
    }
    /**
     * a data type of the column
     */
    private String aDataType;

    public String getDataType() {
        return aDataType;
    }
    /**
     * a table to which the column belongs
     */
    private Table aOwner;

    public Table getOwner() {
        return aOwner;
    }
    /**
     * Distributes values dependent on distribution mode
     */
    private DataDistributor aDataDistributor;

    public DataDistributor getDataDistributor() {
        return aDataDistributor;
    }
    /**
     * a number of rows, which was already generated
     */
    private int alreadyGeneratedRows = 0;

    public int getAlreadyGeneratedRows() {
        return alreadyGeneratedRows;
    }

    /**
     * Adds a generated rows to already generated rows variable
     * 
     * @param aGeneratedRows
     */
    public void registerGeneratedRows(int aGeneratedRows) {
        this.alreadyGeneratedRows += aGeneratedRows;
    }
    /**
     * inclusion dependencies, which belongs to the column
     */
    protected InclusionDependencyCollection aInclusionDependencyCollection = new InclusionDependencyCollection();

    public InclusionDependencyCollection getInclusionDependencyCollection() {
        return aInclusionDependencyCollection;
    }
    protected boolean isValuesNotGenerated = true;

    /**
     * needed for composite inclusion dependencies where 
     * referenced Columns should be empty before generation
     */
    public void markAsGenerated() {
        isValuesNotGenerated = false;
    }
    protected ValuesDataStorage aValuesDataStorage = null;

    public boolean isValuesNotGenerated() {
        return isValuesNotGenerated;
    }
    protected UniqueValuesDataStorage aUniqueValuesDataStorage = null;

    public Column(ColumnModel aColumnModel, Table aOwner) throws Exception {
        initClass(aColumnModel, aOwner, false);
    }

    public Column(ColumnModel aColumnModel, Table aOwner, boolean hasInitialData) throws Exception {
        initClass(aColumnModel, aOwner, hasInitialData);
    }

    private int getRowsCanBeGenerated(int aNumberOfValuesToBeGenerated) {
        int aTotalValuesToGenerate = Math.min(aDataDistributor.getAllValuesCount(), aNumberOfValuesToBeGenerated);
        int aRowsCanBeGenerated = aTotalValuesToGenerate - this.alreadyGeneratedRows;
        return aRowsCanBeGenerated;
    }

    private void initClass(ColumnModel aColumnModel, Table aOwner, boolean hasInitialData) throws Exception {
        this.aName = aColumnModel.name;
        this.aCellSize = aColumnModel.cellSize;
        this.aDataType = aColumnModel.dataType;
        this.aOwner = aOwner;
        this.aDataDistributor = DataDistributor.findDataDistributor(aColumnModel.distribution);
        this.aDataDistributor.setupRange(this.aOwner.getMinRowNumbers());

        this.aValuesDataStorage = new ValuesDataStorage(this.getValuesDataFilePath());
        this.aUniqueValuesDataStorage = new UniqueValuesDataStorage(this.getUniqueValuesDataFilePath());
        if (hasInitialData == false) {
            clean();
        }
    }

    protected String getValuesDataFilePath() {
        return this.getWorkingDirectoryPath() + "\\" + "Values.bin";
    }

    protected String getUniqueValuesDataFilePath() {
        return this.getWorkingDirectoryPath() + "\\" + "UniqueValues.bin";
    }

    public String getWorkingDirectoryPath() {
        return this.aOwner.getWorkingDirectoryPath() + "\\" + this.aName;
    }

    public boolean isDependentOn(Column aColumn) {
        return aInclusionDependencyCollection.isDependentOn(aColumn);
    }

    /**
     * a main function, which generates data for the column
     * 
     * @throws Exception
     */
    public void generateBaseColumnValues() throws Exception {
        generateRequiredColumnValues(getOwner().getMinRowNumbers());
    }

    /**
     * a main function, which generates data for the column
     * 
     * @throws Exception
     */
    public void generateRequiredColumnValues(int aValuesCount) throws Exception {
        if (!isValuesNotGenerated) {
            return;
        }

        aInclusionDependencyCollection.generateDependentColumnsData();

        if (!isValuesNotGenerated) {
            return;
        }

        openForAppend();
        aDataDistributor.setupRange(aValuesCount);
        generateAdditionalRandomDistribution(aValuesCount);
        collectAllDependentData(aValuesCount);
        closeAfterAppend();

        generateAdditionalNoiseDistribution(aValuesCount);

        markAsGenerated();
    }

    /**
     * Generate additional data values in order to balance the number of rows in
     * all columns in a table. Keeps all defined conditions. If a column is a
     * part of a composite column or a column is dependent, only values from the
     * column unique file can be added as additional values to fitting a column.
     * Because the order and values are important.
     * 
     * @throws IOException
     */
    public void generateAdditionalExtendedDistribution(int aNumberOfValuesToBeGenerated) throws Exception {
        extendDistribution(aNumberOfValuesToBeGenerated);
        generateAdditionalNoiseDistribution(aNumberOfValuesToBeGenerated);
    }

    private void extendDistribution(int aNumberOfValuesToBeGenerated) throws Exception {
        aDataDistributor.setupRange(aNumberOfValuesToBeGenerated);

        this.alreadyGeneratedRows = this.getNumberOfValues();
        int aRowsCanBeGenerated = getRowsCanBeGenerated(aNumberOfValuesToBeGenerated);

        aUniqueValuesDataStorage.openForRead();
        aValuesDataStorage.openForAppend();
        UniqueValuesDataStorage aDataStorageForNewUniqueValuesCounts = createAndOpenDataStorageForNewUniqueValuesCounts();

        ValueSeriaInfo aCurrentUniqueValueInfo = null;

        while (aRowsCanBeGenerated > 0
                && (aCurrentUniqueValueInfo = safeReadNextValueSeriaInfo()) != null) {

            int aNewCountOfCurrentUniqueValue = aDataDistributor.getCountOfValue(aCurrentUniqueValueInfo.getValue());
            int aRestOfCountToBeGenerated = aNewCountOfCurrentUniqueValue - aCurrentUniqueValueInfo.getValueCount();
            if (aRestOfCountToBeGenerated < 0) {
                aDataStorageForNewUniqueValuesCounts.appendNewValueSeriaInfo(aCurrentUniqueValueInfo);
                continue;
            }

            aRestOfCountToBeGenerated = Math.min(aRestOfCountToBeGenerated, aRowsCanBeGenerated);

            aCurrentUniqueValueInfo.setValueCount(aRestOfCountToBeGenerated);
            aValuesDataStorage.appendValueSeria(aCurrentUniqueValueInfo);

            this.registerGeneratedRows(aRestOfCountToBeGenerated);
            aRowsCanBeGenerated -= aRestOfCountToBeGenerated;

            aCurrentUniqueValueInfo.setValueCount(aNewCountOfCurrentUniqueValue);
            aDataStorageForNewUniqueValuesCounts.appendNewValueSeriaInfo(aCurrentUniqueValueInfo);
        }
        copyUntouchedUniuqeValuesInto(aDataStorageForNewUniqueValuesCounts);
        aDataStorageForNewUniqueValuesCounts.closeAfterAppend();

        aUniqueValuesDataStorage.closeAfterRead();
        aValuesDataStorage.closeAfterAppend();
        makeDataStorageForNewUniqueValuesCountsAsMain();
    }

    protected final UniqueValuesDataStorage createAndOpenDataStorageForNewUniqueValuesCounts() throws Exception {
        UniqueValuesDataStorage aExtendedUniqueValuesDataStorage = new UniqueValuesDataStorage(getUniqueValuesDataWithNewCountsFilePath());
        aExtendedUniqueValuesDataStorage.clean();
        aExtendedUniqueValuesDataStorage.openForAppend();
        return aExtendedUniqueValuesDataStorage;
    }

    protected final void copyUntouchedUniuqeValuesInto(UniqueValuesDataStorage aDataStorageForNewUniqueValuesCounts) throws Exception {
        for (int aUniqueValueIndex = aDataStorageForNewUniqueValuesCounts.getNumberOfValues(); aUniqueValueIndex < aUniqueValuesDataStorage.getNumberOfValues(); aUniqueValueIndex++) {
            aDataStorageForNewUniqueValuesCounts.appendNewValueSeriaInfo(aUniqueValuesDataStorage.readNextValueSeriaInfo());
        }
    }

    protected final void makeDataStorageForNewUniqueValuesCountsAsMain() throws IOException {
        FileHelper.removeFileOrDirectory(this.getUniqueValuesDataFilePath());
        new File(getUniqueValuesDataWithNewCountsFilePath()).renameTo(new File(this.getUniqueValuesDataFilePath()));
    }

    protected final String getUniqueValuesDataWithNewCountsFilePath() {
        return this.getUniqueValuesDataFilePath() + ".ext";
    }

    private void generateAdditionalNoiseDistribution(int aNumberOfValuesToBeGenerated) throws Exception {
        this.alreadyGeneratedRows = this.getNumberOfValues();
        int aRowsCanBeGenerated = aNumberOfValuesToBeGenerated - this.alreadyGeneratedRows;

        aValuesDataStorage.openForAppend();
        aUniqueValuesDataStorage.openForRead();

        while (aRowsCanBeGenerated > 0) {
            ValueSeriaInfo aCurrentUniqueValueInfo = roundedReadNextValueSeriaInfo();

            int aNewCountOfCurrentUniqueValue = aCurrentUniqueValueInfo.getValueCount() + 1;
            int aRestOfCountToBeGenerated = aNewCountOfCurrentUniqueValue - aCurrentUniqueValueInfo.getValueCount();

            aRestOfCountToBeGenerated = Math.max(aRestOfCountToBeGenerated, 1);
            aRestOfCountToBeGenerated = Math.min(aRestOfCountToBeGenerated, aRowsCanBeGenerated);

            aCurrentUniqueValueInfo.setValueCount(aRestOfCountToBeGenerated);
            aValuesDataStorage.appendValueSeria(aCurrentUniqueValueInfo);

            this.registerGeneratedRows(aRestOfCountToBeGenerated);
            aRowsCanBeGenerated -= aRestOfCountToBeGenerated;

            aCurrentUniqueValueInfo.setValueCount(aNewCountOfCurrentUniqueValue);
            aUniqueValuesDataStorage.updateValueSeriaInfo(aCurrentUniqueValueInfo);
        }

        aValuesDataStorage.closeAfterAppend();
        aUniqueValuesDataStorage.closeAfterRead();
    }

    private ValueSeriaInfo roundedReadNextValueSeriaInfo() throws Exception {
        ValueSeriaInfo result = null;
        try {
            result = aUniqueValuesDataStorage.readNextValueSeriaInfo();
        } catch (EOFException ex) {
            aUniqueValuesDataStorage.closeAfterRead();
            aUniqueValuesDataStorage.openForRead();

            result = aUniqueValuesDataStorage.readNextValueSeriaInfo();
        }
        return result;
    }

    private ValueSeriaInfo safeReadNextValueSeriaInfo() throws Exception {
        ValueSeriaInfo result = null;
        try {
            result = aUniqueValuesDataStorage.readNextValueSeriaInfo();
        } catch (EOFException ex) {
            result = null;
        }
        return result;
    }

    public void closeAfterAppend() throws Exception {
        this.aUniqueValuesDataStorage.closeAfterAppend();
        this.aValuesDataStorage.closeAfterAppend();
    }

    public void openForAppend() throws Exception {
        aValuesDataStorage.openForAppend();
        aUniqueValuesDataStorage.openForAppend();
    }

    public boolean isOpen() throws Exception {
        return (aValuesDataStorage.isOpen() || aUniqueValuesDataStorage.isOpen());
    }

    protected void collectAllDependentData(int aMaxValuesCount) throws Exception {
        for (InclusionDependency aInclusionDependency : this.getInclusionDependencyCollection()) {
            System.out.println("Generating referenced data for " + aInclusionDependency);

            Column aDependentColumn = aInclusionDependency.getDependent();
            collectOneDependentColumnData(aDependentColumn, aMaxValuesCount);
        }
    }

    protected void collectOneDependentColumnData(Column aDependentColumn, int aMaxValuesCount) throws Exception {
        aDependentColumn.openForRead();
        int aNumberOfValues = aDependentColumn.getNumberOfUniqueValues();

        for (int aValueCounter = 0; aValueCounter < aNumberOfValues; aValueCounter++) {
            ValueSeriaInfo aValuesDataEntry = aDependentColumn.readNextUniqueValueSeriaInfo();

            if (aUniqueValuesDataStorage.isSeriaOfValueWasNotDistributed(aValuesDataEntry)) {
                distributeCollectedValueSeria(aValuesDataEntry, aMaxValuesCount);
            }
        }

        aDependentColumn.closeAfterRead();
    }

    public boolean hasDependentColumns() {
        return !aInclusionDependencyCollection.isEmpty();
    }

    private void generateAdditionalRandomDistribution(int aNumberOfValuesToBeGenerated) throws Exception {
        int aRowsCanBeGenerated = getRowsCanBeGenerated(aNumberOfValuesToBeGenerated);

        while (aRowsCanBeGenerated > 0) {
            ValueSeriaInfo aValueSeriaInfo = getDataDistributor().generateNextRandomValueSeriaInfo();


            if (aUniqueValuesDataStorage.isSeriaOfValueWasNotDistributed(aValueSeriaInfo)) {

                aRowsCanBeGenerated -= aValueSeriaInfo.getValueCount();
                distributeGeneratedValueSeria(aValueSeriaInfo, aNumberOfValuesToBeGenerated);
            } else {
            }

        }
    }

    public String getFullColumnInfo() {
        return String.format("{name: %s, datatype: %s, cellSize: %d, distribution: %s}\n", getOwner().getName()
                + ":" + getName(), getDataType(), getCellSize(), getDataDistributor().getName());
    }

    @Override
    public String toString() {
        return aOwner.getName() + ":" + getName();

    }

    public void distributeGeneratedValueSeria(ValueSeriaInfo aValueSeriaInfo, int aMaxValuesCount) throws Exception {

        int aValueRepeats = aDataDistributor.getCountOfValue(aValueSeriaInfo.getValue());
        if (aValueRepeats == 0) {
            return;
        }

        aValueRepeats = adjustValueRepeats(aValueRepeats, aMaxValuesCount);
        aValueSeriaInfo.setValueCount(aValueRepeats);
        ValueSeriaInfo aSavedValueSeriaInfo = appendAndRegisterValueSeriaInfo(aValueSeriaInfo);

        this.registerGeneratedRows(aSavedValueSeriaInfo.getValueCount());
    }

    private int adjustValueRepeats(int aValueRepeats, int aMaxValuesCount) {
        int aRestRowsToGenerate = aMaxValuesCount - this.alreadyGeneratedRows;
        if (aValueRepeats > aRestRowsToGenerate) {
            aValueRepeats = aRestRowsToGenerate;
        }
        return aValueRepeats;
    }

    protected ValueSeriaInfo appendAndRegisterValueSeriaInfo(ValueSeriaInfo aValueSeriaInfo) throws Exception {
        ValueSeriaInfo aSavedValueSeriaInfo = aUniqueValuesDataStorage.appendNewValueSeriaInfo(aValueSeriaInfo);
        aValuesDataStorage.appendValueSeria(aSavedValueSeriaInfo);
        return aSavedValueSeriaInfo;
    }

    public void distributeCollectedValueSeria(ValueSeriaInfo aValueSeriaInfo, int aMaxValuesCount) throws Exception {
        int aValueRepeats = aDataDistributor.getCountOfValue(aValueSeriaInfo.getValue());
        if (aValueRepeats == 0) {
            aValueRepeats = 1;
        }

        aValueSeriaInfo.setValueCount(aValueRepeats);
        ValueSeriaInfo aSavedValueSeriaInfo = appendAndRegisterValueSeriaInfo(aValueSeriaInfo);
        this.registerGeneratedRows(aSavedValueSeriaInfo.getValueCount());
    }

    public int getNumberOfUniqueValues() {
        return aUniqueValuesDataStorage.getNumberOfValues();
    }

    public int getNumberOfValues() {
        return aValuesDataStorage.getNumberOfValues();
    }

    public ValueSeriaInfo readNextUniqueValueSeriaInfo() throws IOException, Exception {
        return aUniqueValuesDataStorage.readNextValueSeriaInfo();
    }

    public ValueSeriaInfo readNextValueSeriaInfo() throws Exception {
        return aValuesDataStorage.readValue();
    }

    public void openForRead() throws Exception {
        aUniqueValuesDataStorage.openForRead();
        aValuesDataStorage.openForRead();
    }

    public void closeAfterRead() throws Exception {
        aUniqueValuesDataStorage.closeAfterRead();
        aValuesDataStorage.closeAfterRead();
    }

    public ValueSeriaInfo appendNewUniqueValueSeriaInfo(ValueSeriaInfo valueSeriaInfo) throws Exception {
        return aUniqueValuesDataStorage.appendNewValueSeriaInfo(valueSeriaInfo);
    }

    public void appendValueSeriaInfo(ValueSeriaInfo valueSeriaInfo) throws Exception {
        aValuesDataStorage.appendValueSeria(valueSeriaInfo);
    }

    public void clean() throws Exception {
        aUniqueValuesDataStorage.clean();
        aValuesDataStorage.clean();
    }

    public String convertToString(int aValue) throws Exception {
        throw new Exception("Must be overwritten");
    }
}
